﻿using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;
using TheSnakeGame.Engine;
using TheSnakeGame.Engine.GameObjects;
using Tests.TheSnakeGame.Engine.Mocks;
using System.Collections.Generic;

namespace Tests.TheSnakeGame.Engine
{
    [TestClass]
    public class EngineTests
    {
        #region Initialize Game Engine
        [TestMethod]
        public void StandartInitialization_EnsureWallsWerePlaced()
        {
            var gameEngine = new GameEngine(10, 10);
            gameEngine.PerfostmStandartInitialization();
            var wallsBricksCount = gameEngine.GameObjects.Count(x => x.GetType() == typeof(BarierObject));

            Assert.AreEqual(36, wallsBricksCount, "Incorrect number of walls for matrix of 10 by 10 was generated - Current walls count: " + wallsBricksCount);
        }

        // TODO: Add other Objects
        [TestMethod]
        public void StandartInitialization_EnsureGameMapHasWalls()
        {
            var gameEngine = new GameEngine(10, 10);
            gameEngine.PerfostmStandartInitialization();
            for (int i = 0; i < 10; i++)
            {
                // buttom wall 
                Assert.AreEqual(1, gameEngine.GameMap[i, 0].Count, "More then wall object at {" + i + ", " + 0 + "}");
                foreach (var obj in gameEngine.GameMap[i, 0])
                {
                    Assert.IsTrue(obj.GetType() == typeof(BarierObject), "Incorrent object placed in wall at {" + i + ", " + 0 + "}");
                }

                // topwall
                Assert.AreEqual(1, gameEngine.GameMap[i, 9].Count, "More then wall object at {" + i + ", " + 9 + "}");
                foreach (var obj in gameEngine.GameMap[i, 9])
                {
                    Assert.IsTrue(obj.GetType() == typeof(BarierObject), "Incorrent object placed in wall at {" + i + ", " + 9 + "}");
                }

                // left wall
                Assert.AreEqual(1, gameEngine.GameMap[0, i].Count, "More then wall object at {" + 0 + ", " + i + "}");
                foreach (var obj in gameEngine.GameMap[0, i])
                {
                    Assert.IsTrue(obj.GetType() == typeof(BarierObject), "Incorrent object placed in wall at {" + 0 + ", " + i + "}");
                }

                // right wall
                Assert.AreEqual(1, gameEngine.GameMap[9, i].Count, "More then wall object at {" + 9 + ", " + i + "}");
                foreach (var obj in gameEngine.GameMap[9, i])
                {
                    Assert.IsTrue(obj.GetType() == typeof(BarierObject), "Incorrent object placed in wall at {" + 9 + ", " + i + "}");
                }
            }
        }
        #endregion

        #region GameTicks
        [TestMethod]
        public void ProcessGameTick_GameObjectWillNotColideTwoTicksAfterInitialization()
        {
            var gameEngine = new GameEngine(10, 10);
            gameEngine.PerfostmStandartInitialization();
            gameEngine.ProcessGameTick();
            gameEngine.ProcessGameTick();

            for (int i = 0; i < gameEngine.GameMap.GetLength(0); i++)
            {
                for (int j = 0; j < gameEngine.GameMap.GetLength(1); j++)
                {
                    Assert.IsTrue(gameEngine.GameMap[i, j].Count <= 1, "Two Ticks after initialization there are two object at the same cell {" + i + ", " + j + "}");
                }
            }
        }

        [TestMethod]
        public void ProcessGameTicks_AssureGameObjectWillNotBeAddedAgainInTheGameMapOnEachProcessGameTick()
        {
            var gameEngine = new GameEngine(10, 10);
            var testObj = new GameObject(new Point(5, 5));
            gameEngine.GameObjects.Add(testObj);
            gameEngine.RefreshGameMap();

            gameEngine.ProcessGameTick();

            Assert.AreEqual(1, gameEngine.GameMap[5, 5].Count, "Object count has unexpectedly changed after ProcessGameTick was called");
            Assert.AreEqual(gameEngine.GameMap[5, 5][0], testObj, "Incorrect obj at expexted position");
        }

        [TestMethod]
        public void ProcessGameTick_AssureForEachGameObjects_DoActionMethod_IsCalled()
        {
            var gameEngine = new GameEngine(10, 10);
            var testObj = new DoActionGameObjectMock(new Point(5, 5));
            gameEngine.GameObjects.Add(testObj);
            gameEngine.RefreshGameMap();

            gameEngine.ProcessGameTick();

            Assert.AreEqual(true, testObj.DoActionWasCalled, "Test Object DoAction() was not called");
        }

        [TestMethod]
        public void ProcessGameTick_ObjectThatDoesNotInteract_ShouldRemainUnchangedAtTheirPossition()
        {
            var gameEngine = new GameEngine(10, 10);
            var firstTestObj = new GameObject(new Point(5, 5));
            var secondTestObj = new GameObject(new Point(5, 5));
            gameEngine.GameObjects.AddRange(new[] { firstTestObj, secondTestObj });
            gameEngine.RefreshGameMap();
            gameEngine.ProcessGameTick();

            Assert.AreEqual(2, gameEngine.GameMap[5, 5].Count, "Incorrect number of object");
            Assert.AreEqual(firstTestObj, gameEngine.GameMap[5, 5][0], "Unexpected object");
            Assert.AreEqual(secondTestObj, gameEngine.GameMap[5, 5][1], "Unexpected object");
        }

        [TestMethod]
        public void ProcessGameTicks_ObjectHasChangedPosition_EnsureTheGameMapIsSynchronizedCOrrectly()
        {
            var gameEngine = new GameEngine(10, 10);
            var testObject = new GameObject(new Point(5, 5));
            gameEngine.GameObjects.Add(testObject);
            gameEngine.RefreshGameMap();
            testObject.Position.X = 6;
            testObject.Position.Y = 6;
            gameEngine.ProcessGameTick();

            Assert.AreEqual(0, gameEngine.GameMap[5, 5].Count, "Object was not moved");
            Assert.AreEqual(1, gameEngine.GameMap[6, 6].Count, "Object is not at located at the expected position");
            Assert.AreEqual(testObject, gameEngine.GameMap[6, 6][0], "Unexpected object located at the target position");
        }

        [TestMethod]
        public void PreccessMap_ObjectsThatAreMarkedForDestructionMustBeRemoved()
        {
            var gameEngine = new GameEngine(10, 10);
            var firstTestObj = new GameObject(new Point(5, 5));
            firstTestObj.IsForDestruction = true;
            var secondTestObj = new GameObject(new Point(6, 5));
            gameEngine.GameObjects.AddRange(new[] { firstTestObj, secondTestObj });
            gameEngine.RefreshGameMap();
            gameEngine.ProcessGameTick();

            Assert.IsFalse(gameEngine.GameObjects.Contains(firstTestObj), "Game object not deleted");
            Assert.IsTrue(gameEngine.GameObjects.Contains(secondTestObj), "Missing game object");
        }

        [TestMethod]
        public void PreccessMap_TwoGameObjectsAtOnePlace()
        {
            var gameEngine = new GameEngine(10, 10);
            var firstTestObj = new GameObject(new Point(5, 5));
            firstTestObj.IsForDestruction = true;
            var secondTestObj = new GameObject(new Point(5, 5));
            gameEngine.GameObjects.AddRange(new[] { firstTestObj, secondTestObj });
            gameEngine.RefreshGameMap();
            gameEngine.ProcessGameTick();

            Assert.IsFalse(gameEngine.GameObjects.Contains(firstTestObj), "Game object not deleted");
            Assert.IsTrue(gameEngine.GameObjects.Contains(secondTestObj), "Missing game object");
        }

        #endregion

        #region AddWalls
        [TestMethod]
        [ExpectedException(typeof(System.InvalidOperationException))]
        public void AddWallsTwice_ShouldGetAnException()
        {
            var engine = new GameEngine(100, 100);
            engine.AddBarierWalls();
            engine.AddBarierWalls();
        }

        [TestMethod]
        public void AddWalls_WallsAreAddedCorrectly()
        {
            var gameEngine = new GameEngine(10, 10);
            gameEngine.AddBarierWalls();
            gameEngine.RefreshGameMap();
            for (int i = 0; i < 10; i++)
            {
                // buttom wall 
                Assert.AreEqual(1, gameEngine.GameMap[i, 0].Count, "More then wall object at {" + i + ", " + 0 + "}");
                foreach (var obj in gameEngine.GameMap[i, 0])
                {
                    Assert.IsTrue(obj.GetType() == typeof(BarierObject), "Incorrent object placed in wall at {" + i + ", " + 0 + "}");
                }

                // topwall
                Assert.AreEqual(1, gameEngine.GameMap[i, 9].Count, "More then wall object at {" + i + ", " + 9 + "}");
                foreach (var obj in gameEngine.GameMap[i, 9])
                {
                    Assert.IsTrue(obj.GetType() == typeof(BarierObject), "Incorrent object placed in wall at {" + i + ", " + 9 + "}");
                }

                // left wall
                Assert.AreEqual(1, gameEngine.GameMap[0, i].Count, "More then wall object at {" + 0 + ", " + i + "}");
                foreach (var obj in gameEngine.GameMap[0, i])
                {
                    Assert.IsTrue(obj.GetType() == typeof(BarierObject), "Incorrent object placed in wall at {" + 0 + ", " + i + "}");
                }

                // right wall
                Assert.AreEqual(1, gameEngine.GameMap[9, i].Count, "More then wall object at {" + 9 + ", " + i + "}");
                foreach (var obj in gameEngine.GameMap[9, i])
                {
                    Assert.IsTrue(obj.GetType() == typeof(BarierObject), "Incorrent object placed in wall at {" + 9 + ", " + i + "}");
                }
            }
        }
        #endregion

        #region AddFood

        [TestMethod]
        public void AddFood_InitOneFoodAtStart()
        {
            var gameEngine = new GameEngine(10, 10);
            gameEngine.PerfostmStandartInitialization();
            gameEngine.RefreshGameMap();
            gameEngine.ProcessGameTick();

            int existingFoodObjectsCount = 0;
            for (int i = 0; i < gameEngine.GameMap.GetLength(0); i++)
            {
                for (int j = 0; j < gameEngine.GameMap.GetLength(1); j++)
                {
                    existingFoodObjectsCount += gameEngine.GameMap[i, j].OfType<Food>().Count();
                }
            }

            Assert.AreEqual(1, existingFoodObjectsCount, "The food count that must be initialize at the begining is 1");
        }

        [TestMethod]
        public void AddFood_GetCorrectCellsFromRandomGenerator()
        {
            RandomGeneratorMock generator = new RandomGeneratorMock();
            RandomGeneratorMock.number = -1;
            var gameEngine = new GameEngine(10, 10, new FoodManager(), generator);
            List<Point> freeCells = gameEngine.GetFreePositions();
            IList<Point> cellsForPlacingFood = gameEngine.GetRandomFreeCells(1);
            var comparier = new PointComperer();
            bool areEqual = comparier.Equals(freeCells[0], cellsForPlacingFood[0]);
            Assert.IsTrue(areEqual, "This must take the first free cell.");
        }

        [TestMethod]
        public void AddFood_PerfostmStandartInitialization_AddFood()
        {
            RandomGeneratorMock generator = new RandomGeneratorMock();
            RandomGeneratorMock.number = -1;
            var gameEngine = new GameEngine(10, 10, new FoodManager(), generator);
            List<Point> freeCells = gameEngine.GetFreePositions();
            gameEngine.PerfostmStandartInitialization();//add food and walls, add them to GameObjects

            Point placedCell = freeCells[RandomGeneratorMock.number];
            IList<GameObject> foods = gameEngine.GameMap[placedCell.X, placedCell.Y];

            Assert.AreEqual(1, foods.Count, "Only one food must be placed");
            Assert.AreEqual(1, foods.OfType<Food>().Count(), "This must be food");
        }

        [TestMethod]
        public void AddFood_DeleteMarkedFoodAndAddNewOne()
        {
            RandomGeneratorMock generator = new RandomGeneratorMock();
            RandomGeneratorMock.number = -1;
            var gameEngine = new GameEngine(10, 10, new FoodManager(), generator);
            List<Point> freeCells = gameEngine.GetFreePositions();
            gameEngine.PerfostmStandartInitialization();//add food and walls, add them to GameObjects

            Point placedCell = freeCells[RandomGeneratorMock.number];
            IList<GameObject> foods = gameEngine.GameMap[placedCell.X, placedCell.Y];

            Assert.AreEqual(1, foods.Count, "Only one food must be placed");
            Assert.AreEqual(1, foods.OfType<Food>().Count(), "This must be food");

            foods[0].IsForDestruction = true;
            //dalete from GameObject and then add new one
            gameEngine.ProcessGameTick();

            //here we do not have old food any more, and the new one is placed
            Assert.IsTrue(gameEngine.GameObjects.Contains(foods[0]) == false);

            IEnumerable<Food> NewFood = gameEngine.GameObjects.OfType<Food>();

            Assert.AreEqual(1, NewFood.Count(), "Only one food must be placed");
            Assert.AreEqual(1, NewFood.OfType<Food>().Count(), "This must be food");
            Assert.AreNotEqual(foods[0], NewFood.FirstOrDefault(), "New food must be instantinate.");
        }

        #endregion

        [TestMethod]
        [ExpectedException(typeof(System.InvalidOperationException))]
        public void AddInitialSnakeTwice_ShouldGetAnException()
        {
            var width = 10;
            var height = 10;

            var gameEngine = new GameEngine(width, height);
            gameEngine.AddInitialSnake();
            gameEngine.AddInitialSnake();
        }

        [TestMethod]
        [ExpectedException(typeof(System.InvalidOperationException))]
        public void AddInitialSnake_HeadAtNonFreePosition_ShouldGetAnException()
        {
            var width = 10;
            var height = 10;

            var gameEngine = new GameEngine(width, height);
            var snakeX = width / 2;
            var snakeY = height / 2;
            var length = (height + width) / 20;
            gameEngine.GameObjects.Add(new GameObject(new Point(snakeX, snakeY)));

            gameEngine.AddInitialSnake();
            gameEngine.RefreshGameMap();
        }

        [TestMethod]
        [ExpectedException(typeof(System.InvalidOperationException))]
        public void AddInitialSnake_BodyAtNonFreePosition_ShouldGetAnException()
        {
            var width = 10;
            var height = 10;

            var gameEngine = new GameEngine(width, height);
            var snakeX = width / 2;
            var snakeY = height / 2;
            var length = (height + width) / 20;
            gameEngine.GameObjects.Add(new GameObject(new Point(snakeX + 1, snakeY)));

            gameEngine.AddInitialSnake();
            gameEngine.RefreshGameMap();
        }

        [TestMethod]
        public void AddInitialSnake_EnsureGameLengthWillBeAtLeastThree()
        {
            var width = 10;
            var height = 10;

            var gameEngine = new GameEngine(width, height);
            var snakeX = width / 2;
            var snakeY = height / 2;
            var length = (height + width) / 20;
            gameEngine.AddInitialSnake();

            Assert.AreEqual(3, gameEngine.Snake.Length, "Snake cannot be shorted then three pieces");
        }

        [TestMethod]
        public void AddInitialSnake_ShouldBeAddedAtTheCorrectCoordinates()
        {
            var width = 10;
            var height = 10;

            var gameEngine = new GameEngine(width, height);
            gameEngine.AddInitialSnake();
            gameEngine.RefreshGameMap();

            var snakeX = width / 2;
            var snakeY = height / 2;
            var length = 3;

            Assert.AreEqual(snakeX, gameEngine.Snake.Position.X, "Snake head is at incorrect position");
            Assert.AreEqual(snakeY, gameEngine.Snake.Position.Y, "Snake head is at incorrect position");
            Assert.AreEqual(length, gameEngine.Snake.Length, "Snake is not with correct length");

            var i = 0;
            foreach (var bodyPiece in gameEngine.Snake.Body)
            {
                Assert.AreEqual(snakeX + i + 1, bodyPiece.Position.X, "Snake part not placed at correct width");
                Assert.AreEqual(snakeY, bodyPiece.Position.Y, "Snake part not placed at correct height");
                i++;
            }
        }
    }
}
